package com.zhuangjie.idworker.helper;

import com.zhuangjie.idworker.core.SnowflakeIdWorker;
import com.zhuangjie.idworker.properties.SnowflakeHelperProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
@EnableConfigurationProperties(SnowflakeHelperProperties.class)
public class SnowflakeHelper extends SnowflakeIdWorker {


    @Autowired(required = false)
    private RedisTemplate redisTemplate;

    public SnowflakeHelperProperties config;

    @Autowired
    private ServiceNoHelper serviceNoHelper;

    /**
     * 是否可用
     */
    private volatile boolean isAvailable = false;


    /**
     * 单机默认服务号
     */
    private static final long STANDALONE_SERVICE_NO = 0L;

    public SnowflakeHelper(SnowflakeHelperProperties config) {
        super(config.getDateCenterId());
        this.config = config;
    }

    @Autowired
    private void init() {
        if (config.isStandaloneMode()) {
            // 不需要多服务交互获取服务号
            setWorkerId(STANDALONE_SERVICE_NO);
            // IdWorker当以单机模式运行，当存在同服务多模块下生成的id可能相同！
            log.info("IdWorker：正在以单机模式运行！");
        } else {
            if (redisTemplate == null) {
                log.warn("IdWorker：当前非单机模式，所以要求当前环境不存在redis环境.");
                throw new RuntimeException("当前非单机模式,但不存在redis，如使用单机模式，请通过配置声明！");
            }
            // 非单机初始化workerId
            if (!tryGetAndSwitchWorkerId(config.getDateCenterId(), config.getModuleName(), config.getExpirationTimeSeconds())) {
                throw new RuntimeException("无法获取服务号！");
            }
            log.info("idWorker: 当前服务的服务号是" + getWorkerId() + ".");
        }
        isAvailable = true;
        if (config.isStandaloneMode()) return;
        // 非单机-看门狗续时
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        // 创建一个 Runnable（可执行任务）
        Runnable task = () -> {
            long questionableWorkerId = super.getWorkerId();
            boolean isRenewalSuccessful = tryContinuation(questionableWorkerId);
            // 续时成功，本次定时任务完成
            if (isRenewalSuccessful) return;
            // 续时失败，进入死循环进行不断尝试恢复
            isAvailable = false;
            // 一轮最大重试次数
            int maxRetryTimes = 3;
            while (maxRetryTimes-- > 0) {
                try {
                    if (!serviceNoHelper.tryStateRecovery(questionableWorkerId)) {
                        log.error("idWorker:  尝试恢复状态失败，准备获取新的服务号");
                        // 续时失败且尝试恢复状态失败，准备获取新的服务号
                        if (! tryGetAndSwitchWorkerId(config.getDateCenterId(), config.getModuleName(), config.getExpirationTimeSeconds())) {
                            log.error("idWorker: 无法获取新的服务号！准备进行下一轮的重试！");
                            throw new RuntimeException("获取新的服务号失败！");
                        }
                    }
                    log.info("idWorker: 已恢复正常！");
                    isAvailable = true;
                    break;
                } catch (Exception e) {
                    try {
                        // 每次重试失败停顿100ms
                        Thread.sleep(100);
                    } catch (InterruptedException ex) {
                    }
                }

            }

        };

        // 定义延迟时间和重复间隔（以毫秒为单位）
        long delay = 0; // 延迟时间，表示首次执行任务的延迟时间

        // 安排任务
        executor.scheduleAtFixedRate(task, delay, (long) (config.getExpirationTimeSeconds() * 0.65), TimeUnit.SECONDS);
        log.info("idWorker: 启动看门狗守卫状态！ing");
    }

    /**
     * 尝试续时
     *
     * @param workerId 工作人员id
     * @return boolean
     */
    private boolean tryContinuation(long workerId) {
        try {
            CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() ->
                    redisTemplate.expire(serviceNoHelper.ASSEMBLE_PREFIX + workerId, this.config.getExpirationTimeSeconds(), TimeUnit.SECONDS));
            // 超时获取
            return future.get(1, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("idWorker: 获取续时结果超时！");
        }
        return false;
    }

    /**
     * 尝试切换workerId
     *
     * @param dateCenterId          日期中心id
     * @param moduleName            模块名称
     * @param expirationTimeSeconds 过期时间秒
     * @return boolean
     */
    private boolean tryGetAndSwitchWorkerId(Integer dateCenterId, String moduleName, Integer expirationTimeSeconds) {
        try {
            // 获取新的服务号（workerId）
            Long newWorkerId = serviceNoHelper.getServiceNo(config.getDateCenterId(), config.getModuleName(), config.getExpirationTimeSeconds());
            // 当获取失败，抛出异常
            if (newWorkerId == null || newWorkerId.equals(-1)) throw new Exception("idWorker: 获取新号失败！");
            if (workerId != newWorkerId) log.info("idWorker: 获取新的服务号成功，服务号已由{}切换至{}.", workerId, newWorkerId);
            setWorkerId(newWorkerId);
            return true;
        } catch (Exception e) {
            log.error("idWorker: 获取新号失败！原因：", e.getMessage());
            return false;
        }
    }

    private boolean tryShortBlockingWait(long lastTimestamp) {

        // 尝试阻塞等待
        long timestamp = timeGen();
        if (lastTimestamp - timestamp <= 5) {
            // 如果回退时间非常短，进行阻塞等待 （5ms）
            while (lastTimestamp - timeGen() <= 0) {
            }
            log.info("idWorker: 短时间阻塞解决时间回拨（<5ms超短时间）！.");
            return true;
        } else if (lastTimestamp - timestamp <= 30) {
            // 如果回退时间非常短，进行睡眠等待 （30ms）
            try {
                Thread.sleep(lastTimestamp - timestamp);
            } catch (InterruptedException e) {
                return false;
            }
            log.info("idWorker: 短时间阻塞解决时间回拨（<30ms短时间）.");
            return true;
        }
        log.info("idWorker: 无法通过阻塞的方式解决时间回拨.");
        return false;
    }

    // 检查idWorker是否可用
    public synchronized void checkAvailable() {
        if (!isAvailable) {
            throw new RuntimeException("idWorker: 当前不可用！");
        }
    }

    public synchronized long nextId() {
        checkAvailable();
        long timestamp = timeGen();
        // 如果当前时间小于上一次ID生成的时间戳，说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            log.warn("idWorker: 出现了时间回拨！相差{}毫秒.", lastTimestamp - timestamp);
            if (!tryShortBlockingWait(lastTimestamp)) {
                // 当回拨时间过长时，就不能进行短时间阻塞
                if (config.isStandaloneMode())
                    throw new RuntimeException("idWorker: 单机模式无法解决长‘时间回拨’问题！");
                log.info("idWorker: 基于多模块模式，正在通过切换workerId方式解决");
                // 通过切换workerId方式解决
                try {
                    freezeCurrentWorkerIdAndSwitchWorkerId(workerId, lastTimestamp);
                } catch (Exception e) {
                    // 当出现异常，尝试判断当前是否已经到达lastTimestamp了，如果到达,时间回拨问题就没了
                    if ((timestamp = timeGen()) < lastTimestamp) {
                        String clockMovedBackwardsDesc = String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp);
                        log.error("idWorker: 遇到系统时钟回退，且无法切换到的新workerId上！" + clockMovedBackwardsDesc);
                        throw new RuntimeException(e);
                    }
                }

            }
        }
        checkAvailable();
        return super.nextId();
    }


    /**
     * 冻结当前工作进程id和切换工作进程id
     *
     * @param clockBackwardWorkerId 产生时钟回退的workerId     工作人员id
     * @param lastTimestamp         上次时间戳
     * @return long
     */
    public synchronized void freezeCurrentWorkerIdAndSwitchWorkerId(long clockBackwardWorkerId, long lastTimestamp) throws Exception {
        // 等待expire毫秒后使用该workerId产生的id才可能是正常的，因为之前的可能已经生成过了
        long expire = lastTimestamp - timeGen();
        // 冻结原workerId至时间回拨前的时间（不能让其它同服务获取该进行生成id，否则也会出问题）
        CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() ->
                redisTemplate.expire(serviceNoHelper.ASSEMBLE_PREFIX + clockBackwardWorkerId, expire, TimeUnit.MILLISECONDS));
        Boolean isFreezeWorkerIdSuccessful = future.get(1, TimeUnit.SECONDS);
        if (!isFreezeWorkerIdSuccessful) throw new Exception("冻结WorkerId失败！");
        // 切换新的workerId
        if (!tryGetAndSwitchWorkerId(config.getDateCenterId(), config.getModuleName(), config.getExpirationTimeSeconds())) {
            throw new Exception("无法获取新的workerId进行切换!");
        }
    }
}
